package cn.thornbird.orgsync.service.impl;

import cn.thornbird.orgsync.entity.Attachment;
import cn.thornbird.orgsync.exception.BadRequestException;
import cn.thornbird.orgsync.exception.InternalServerErrorException;
import cn.thornbird.orgsync.exception.NotFoundException;
import cn.thornbird.orgsync.repository.AttachmentRepository;
import cn.thornbird.orgsync.service.AttachmentService;
import cn.thornbird.orgsync.service.DownloadCallback;
import cn.thornbird.orgsync.util.DateUtil;
import cn.thornbird.orgsync.util.StreamUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.UriUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

@Service
@Transactional(rollbackFor = Exception.class)
@Slf4j
public class AttachmentServiceImpl extends BaseService<AttachmentRepository, Attachment> implements AttachmentService {

    private final AttachmentRepository attachmentRepository;

    @Value("${application.public-path}")
    private String publicPath;

    @Value("${application.temp-path}")
    private String tempPath;

    @Value("${application.attachment-path}")
    private String attachmentPath;

    @Value("${application.extensions}")
    private String extensions;

    @Value("${application.invalid-attachment-max-age}")
    private Long invalidAttachmentMaxAge;

    public AttachmentServiceImpl(AttachmentRepository attachmentRepository) {
        this.attachmentRepository = attachmentRepository;
    }

    @Override
    public Attachment find(String id) throws NotFoundException {
        return findFirstById(attachmentRepository, id);
    }

    @Override
    public Attachment findByFileUrl(String fileUrl) {
        return attachmentRepository.findFirstByFileUrl(fileUrl);
    }

    @Override
    public void download(String attachmentId, HttpServletResponse response) {
        download(attachmentId, null, null, response);
    }

    @Override
    public void download(String attachmentId, String filename, DownloadCallback downloadCallback,
                         HttpServletResponse response) {
        boolean hasError = false;
        boolean exist = false;
        File file = null;
        Attachment attachment = attachmentRepository.findFirstById(attachmentId);
        if (attachment != null) {
            file = new File(attachment.getFileUrl());
            exist = file.exists();
        }
        if (exist) {
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.setContentType(attachment.getMimeType());
            if (StringUtils.isEmpty(filename)) {
                filename = attachment.getFilename();
            } else {
                String extension = "";
                if (attachment.getFilename().contains(".")) {
                    extension = attachment.getFilename().substring(
                            attachment.getFilename().lastIndexOf(".")).toLowerCase();
                }
                if (!extension.isEmpty() && !filename.toLowerCase().endsWith(extension)) {
                    filename = filename + extension;
                }
            }
            try {
                response.addHeader(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment;filename=%s;filename*=%s",
                        UriUtils.encode(filename, StandardCharsets.UTF_8), StandardCharsets.UTF_8.name()));
                InputStream inputStream = Files.newInputStream(Paths.get(file.getAbsolutePath()));
                if (downloadCallback != null) {
                    downloadCallback.callback(inputStream, response.getOutputStream());
                } else {
                    StreamUtil.writeOutputStream(inputStream, response.getOutputStream());
                }
            } catch (IOException e) {
                log.error("error", e);
                hasError = true;
            }
        } else {
            hasError = true;
        }
        if (hasError) {
            response.setContentType("text/plain");
            response.addHeader(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment;filename=%s;filename*=%s",
                    UriUtils.encode("error.txt", StandardCharsets.UTF_8), StandardCharsets.UTF_8.name()));
            try {
                response.getWriter().println("error");
                response.getWriter().close();
            } catch (IOException e) {
                log.error("error", e);
            }
        }
    }

    @Override
    public Attachment save(MultipartFile multipartFile) throws BadRequestException, InternalServerErrorException {
        Attachment attachment = new Attachment();
        String filename = multipartFile.getOriginalFilename();
        final String extension;
        if (filename != null && filename.contains(".")) {
            extension = filename.substring(filename.lastIndexOf(".")).toLowerCase();
        } else {
            extension = "";
        }
        List<String> list = Arrays.stream(extensions.split(",")).filter(e -> e.toLowerCase().equals(extension))
                .collect(Collectors.toList());
        if (list.isEmpty()) {
            throw new BadRequestException("file extension not allowed");
        }

        attachment.generateNewId();
        attachment.setFilename(filename);
        attachment.setExtension(extension);
        attachment.setFileSize(multipartFile.getSize());

        StringBuilder stringBuilder = new StringBuilder();
        String monthPath = DateUtil.formatDate(new Date(), DateUtil.PURE_DATE_FORMAT).substring(0, 6);
        filename = attachment.getId() + attachment.getExtension();
        stringBuilder.append(publicPath).append("/").append(tempPath).append("/").append(monthPath)
                .append("/").append(filename);
        attachment.setFileUrl(stringBuilder.toString());
        File file = new File(attachment.getFileUrl());
        if (!file.getParentFile().exists()) {
            file.getParentFile().mkdirs();
        }

        Path path = Paths.get(file.getAbsolutePath());
        String mimeType = null;
        try {
            mimeType = Files.probeContentType(path);
        } catch (IOException e) {
            log.error("error", e);
        }
        if (mimeType == null) {
            mimeType = "application/octet-stream";
        }
        attachment.setMimeType(mimeType);

        try {
            InputStream inputStream = multipartFile.getInputStream();
            writeFile(inputStream, path);
        } catch (IOException e) {
            log.error("error", e);
            throw new InternalServerErrorException();
        }

        attachment = attachmentRepository.save(attachment);
        return attachment;
    }

    @Override
    public Attachment move(String id) throws NotFoundException, InternalServerErrorException {
        Attachment attachment = findFirstById(attachmentRepository, id);
        StringBuilder stringBuilder = new StringBuilder();
        String tempPrefix = stringBuilder.append(publicPath).append("/").append(tempPath).append("/").toString();

        if (attachment.getFileUrl().startsWith(tempPrefix)) {
            File file = new File(attachment.getFileUrl());
            stringBuilder = new StringBuilder();
            String targetFileUrl = stringBuilder.append(publicPath).append("/")
                    .append(attachmentPath).append("/")
                    + attachment.getFileUrl().substring(tempPrefix.length());
            File targetFile = new File(targetFileUrl);
            Path path = Paths.get(targetFile.getAbsolutePath());
            if (!targetFile.getParentFile().exists()) {
                targetFile.getParentFile().mkdirs();
            }

            try {
                InputStream inputStream = Files.newInputStream(Paths.get(file.getAbsolutePath()));
                writeFile(inputStream, path);
            } catch (IOException e) {
                log.error("error", e);
                throw new InternalServerErrorException();
            }
            if (file.exists()) {
                file.delete();
            }

            attachment.setFileUrl(targetFileUrl);
            attachment = attachmentRepository.save(attachment);
        }
        return attachment;
    }

    @Override
    public Attachment delete(String id) throws NotFoundException {
        Attachment attachment = findFirstById(attachmentRepository, id);
        attachmentRepository.logicDelete(attachment.getId());
        File file = new File(attachment.getFileUrl());
        if (file.exists()) {
            file.delete();
        }
        return attachment;
    }

    @Override
    public void deleteTemporaryAttachments() throws NotFoundException {
        StringBuilder stringBuilder = new StringBuilder();
        List<Attachment> attachments = attachmentRepository.findTop100ByFileUrlStartingWithOrderByCreatedAtDesc(
                stringBuilder.append(publicPath).append("/").append(tempPath).append("/").toString());
        long maxAge = invalidAttachmentMaxAge == null ? 180000 : invalidAttachmentMaxAge;
        long now = new Date().getTime();
        for (Attachment attachment : attachments) {
            if (attachment.getCreatedAt().getTime() + maxAge < now) {
                delete(attachment.getId());
            }
        }
    }

    private void writeFile(InputStream inputStream, Path path) throws IOException {
        OutputStream outputStream = Files.newOutputStream(path);
        StreamUtil.writeOutputStream(inputStream, outputStream);
    }

}
